# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import base64
import enum
import hashlib
import hmac
import logging
import time
import urllib.parse
from typing import Union, List

import requests
import requests.auth

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.okex_futures.types import OkexFuturesOrderSide
from coin.exchange.util.async_http_client import create_async_http_client

OkexFuturesOrderSide = OkexFuturesOrderSide


class OkexMarketType(enum.Enum):
  SPOT = 'spot'
  MARGIN = 'margin'
  FUTURES = 'futures'
  ETT = 'ett'
  SWAP = 'swap'


class OkexLedgerType(enum.IntEnum):
  TRANSFER_IN = 1
  TRANSFER_OUT = 2
  BORROW = 3
  REPAY = 4
  INTEREST = 5
  LIQUIDATION_LOSS = 6
  BUY = 7
  SELL = 8


class OkexOrderStatus(enum.Enum):
  ALL = 'all'
  OPEN = 'open'
  PART_FILLED = 'part_filled'
  CANCELING = 'canceling'
  FILLED = 'filled'
  CANCELED = 'cancelled'
  FAILURE = 'failure'
  ORDERING = 'ordering'


class OkexFuturesOrderStatus(enum.IntEnum):
  FAILED = -2
  CANCELED = -1
  NEW = 0  # no fill yet
  PART_FILLED = 1
  FILLED = 2
  ORDERING = 3
  CANCELING = 4
  OPEN = 6  # no fill + partial fill
  DONE = 7  # canceled + fully filled


class OkexLoanStatus(enum.IntEnum):
  OUTSTANDING = 0
  REPAID = 1


class OkexMarginMode(enum.Enum):
  CROSSED = 'crossed_mode'
  FIXED = 'fixed_mode'


class OkexOrderType(enum.Enum):
  LIMIT = 'limit'
  MARKET = 'market'


class OkexOrderProperty(enum.IntEnum):
  NORMAL_LIMIT_ORDER = 0
  POST_ONLY = 1
  FOK = 2
  IOC = 3


class OkexLeverageType(enum.IntEnum):
  x10 = 10
  x20 = 20


class OkexOrderSide(enum.Enum):
  BUY = 'buy'
  SELL = 'sell'

  # '1:open long, 2:open short, 3:close long, 4:close short'
  UNKNOWN = 0
  BUY_OPEN = 1
  SELL_OPEN = 2
  SELL_CLOSE = 3
  BUY_CLOSE = 4


OKEX_API_HOST = 'www.okex.com'


class OkexAuth(requests.auth.AuthBase):
  def __init__(self, key_file):
    self._key = AuthKey.from_file(key_file)
    self._key.passphrase = self._key.get_value('passphrase')

  def get_sign(self, str_to_be_signed):
    signature = hmac.new(self._key.secret_key_bytes,
                         str_to_be_signed.encode(),
                         digestmod=hashlib.sha256)
    signature = base64.b64encode(signature.digest()).decode()
    return signature

  def __call__(self, req: requests.PreparedRequest):
    assert req.method in ('GET', 'POST', 'DELETE'), req.method
    timestamp = '%.03f' % time.time()
    parsed_result = urllib.parse.urlparse(req.url)
    if req.method == 'GET' and parsed_result.query:
      path = parsed_result.path + '?' + parsed_result.query
    else:
      path = parsed_result.path

    body = req.body.decode() if req.body else ''
    str_to_be_signed = ''.join([timestamp, req.method, path, body])
    headers = {
        'OK-ACCESS-KEY': self._key.access_key,
        'OK-ACCESS-SIGN': self.get_sign(str_to_be_signed),
        'OK-ACCESS-TIMESTAMP': timestamp,
        'OK-ACCESS-PASSPHRASE': self._key.passphrase,
        'Content-Type': 'application/json',
    }
    req.headers.update(headers)
    return req


class OkexSyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, disable_ssl=False, timeout=10):
    self._timeout = timeout
    self._auth = OkexAuth(key_file) if key_file else None
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or OKEX_API_HOST)
    self.rate_limit = RateLimiter(100, 10)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, method, path, params):
    assert method in ('GET', 'POST', 'DELETE'), method
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    data = None
    if method != 'GET':
      data = params
      params = None

    try:
      response = requests.request(method,
                                  url,
                                  params=params,
                                  json=data,
                                  auth=self._auth,
                                  timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except ValueError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


class OkexAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
      rest_lib='pycurl',
  ):
    self._timeout = timeout
    self._http_client = create_async_http_client(rest_lib=rest_lib, max_connections=max_connections)
    self._auth = OkexAuth(key_file) if key_file else None
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or OKEX_API_HOST)

  def query(self, method, path, params=None):
    assert method in ('GET', 'POST', 'DELETE'), method
    data = None
    if method != 'GET':
      data = params
      params = None

    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(method=method,
                                     url=url,
                                     params=params,
                                     json=data,
                                     auth=self._auth,
                                     timeout=self._timeout)
    return future


class OkexNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
      rest_lib='pycurl',
  ):
    rest_lib = rest_lib or None
    if use_async:
      self._auth_queryer = OkexAsyncQueryer(key_file,
                                            api_host=api_host,
                                            disable_ssl=disable_ssl,
                                            timeout=timeout,
                                            max_connections=max_connections,
                                            rest_lib=rest_lib)
    else:
      self._auth_queryer = OkexSyncQueryer(key_file, api_host=api_host, timeout=timeout)

  def _query(self, method, path, params=None):
    if params is not None:
      params = {key: value for key, value in params.items() if value is not None}
    params = params or None
    return self._auth_queryer.query(method, path, params=params)

  def query_wallet(self):
    method = 'GET'
    path = '/api/account/v3/wallet'
    update = self._query(method, path)
    return update

  def query_accounts(self):
    method = 'GET'
    path = '/api/spot/v3/accounts'
    update = self._query(method, path)
    return update

  def query_margin_accounts(self):
    method = 'GET'
    path = '/api/margin/v3/accounts'
    update = self._query(method, path)
    return update

  def query_single_account(self, *, currency: str):
    method = 'GET'
    path = '/api/spot/v3/accounts/{currency}'
    path = path.format(currency=currency)
    update = self._query(method, path)
    return update

  def query_ledger(
      self,
      *,
      from_num=None,
      to_num=None,
      limit=None,
      currency=None,
  ):
    method = 'GET'
    path = '/api/spot/v3/accounts/{currency}/ledger'
    path = path.format(currency=currency)
    if limit is not None:
      assert 1 <= limit <= 100, limit
    params = {
        'from': from_num,
        'to': to_num,
        'limit': limit,
    }
    update = self._query(method, path, params)
    return update

  def query_order_list(
      self,
      *,
      status: List[OkexOrderStatus],
      instrument_id: str,
      from_num=None,
      to_num=None,
      limit=None,
  ):
    method = 'GET'
    if limit is not None:
      assert 1 <= limit <= 100, limit
    path = '/api/spot/v3/orders'
    params = {
        'status': '|'.join(s.value for s in status),
        'instrument_id': instrument_id,
        'from': from_num,
        'to': to_num,
        'limit': limit,
    }
    update = self._query(method, path, params)
    return update

  def query_open_order_list(self, from_num=None, to_num=None, limit=None, instrument_id=None):
    method = 'GET'
    path = '/api/spot/v3/orders_pending'
    params = {
        'from': from_num,
        'to': to_num,
        'limit': limit,
        'instrument_id': instrument_id,
    }
    update = self._query(method, path, params)
    return update

  def query_order_detail(self, order_id, instrument_id):
    method = 'GET'
    path = '/api/spot/v3/orders/{order_id}'
    path = path.format(order_id=order_id)
    params = {
        'instrument_id': instrument_id,
    }
    update = self._query(method, path, params)
    return update

  def query_transaction_details(
      self,
      order_id,
      instrument_id,
      from_num=None,
      to_num=None,
      limit=None,
  ):
    method = 'GET'
    path = '/api/spot/v3/fills'
    params = {
        'order_id': order_id,
        'instrument_id': instrument_id,
        'from': from_num,
        'to': to_num,
        'limit': limit,
    }
    update = self._query(method, path, params)
    return update

  def query_withdraw_history(self, currency):
    method = 'GET'
    path = '/api/account/v3/withdrawal/history/{currency}'
    path = path.format(currency=currency)
    update = self._query(method, path)
    return update

  def query_deposit_history(self, currency):
    method = 'GET'
    path = '/api/account/v3/deposit/history/{currency}'
    path = path.format(currency=currency)
    update = self._query(method, path)
    return update

  def place_order(
      self,
      *,
      client_oid=None,
      order_type: OkexOrderType,
      order_side: OkexOrderSide,
      order_property: Union[None, OkexOrderProperty] = None,
      instrument_id: str,
      price=None,
      size=None,
      notional=None,
  ):
    method = 'POST'
    path = '/api/spot/v3/orders'
    assert order_type == OkexOrderType.LIMIT, order_type
    assert notional is None, notional
    if order_property is not None:
      order_property = order_property.value

    params = {
        'client_oid': client_oid,
        'type': order_type.value,
        'side': order_side.value,
        'instrument_id': instrument_id,
        'price': price,
        'size': size,
        'notional': notional,
        'order_type': order_property,  # our terminology is different from okex's
        'margin_trading': 1,  # Fix value 1 for spot
    }
    update = self._query(method, path, params)
    return update

  def cancel_order(self, order_id, instrument_id, client_oid=None):
    method = 'POST'
    path = '/api/spot/v3/cancel_orders/{order_id}'
    path = path.format(order_id=order_id)
    params = {
        'client_oid': client_oid,
        'instrument_id': instrument_id,
    }
    update = self._query(method, path, params)
    return update

  def batch_place_orders(self, *, order_data: list):
    method = 'POST'
    path = '/api/spot/v3/batch_orders'
    assert len(order_data) <= 5, len(order_data)
    update = self._query(method, path, order_data)
    return update

  def batch_cancel_orders(self, instrument_id, order_id_list=None):
    method = 'POST'
    path = '/api/spot/v3/cancel_batch_orders'
    params = {
        'instrument_id': instrument_id,
        'order_ids': order_id_list,
    }
    update = self._query(method, path, params)
    return update
